﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Security.AccessControl;
using RoketPack.Collections;
using System.Xml;
using System.Net;

namespace RoketPack.Management
{
    /// <summary>
    /// This class manages caching of application data to minimize outbound requests
    /// and to only update cached data when necessary.
    /// </summary>
    class Cache
    {
        /// <summary>
        /// Ensures that the cache is in a consistant state and is ready for retrieving 
        /// data from it.
        /// </summary>
        private static void EnsureCacheConsistancy()
        {
            // Check Cache directory exists.
            if (!Directory.Exists("Cache"))
                Directory.CreateDirectory("Cache");
        }

        /// <summary>
        /// Fetchs the existing XML document from the cache, or downloads the new document
        /// from srcurl and places it in the cache (and returns the contents of the download).
        /// </summary>
        /// <param name="repo">The repository URL of the application.</param>
        /// <param name="id">The ID of the application.</param>
        /// <param name="srcurl">The URL from which to download the XML document if it doesn't exist in the cache.</param>
        /// <returns>The raw XML data.</returns>
        internal static byte[] GetApplicationData(string repo, string id, string srcurl)
        {
            Cache.EnsureCacheConsistancy();
            string path = Cache.GetPathToCachedXML(repo, id);

            if (File.Exists(path))
            {
                // Use the cached object.
                StreamReader reader = new StreamReader(path);
                string rdata = reader.ReadToEnd();
                reader.Close();

                // Ensure that the XML document is valid (structure-wise).
                try
                {
                    XmlReader xreader = XmlReader.Create(new MemoryStream(Manager.Encoding.GetBytes(rdata)));
                    while (xreader.Read()) ;
                    xreader.Close();

                    // Valid XML.
                    return Manager.Encoding.GetBytes(rdata);
                }
                catch (XmlException)
                {
                    // Invalid XML.  Re-download from the site.
                }
            }

            if (srcurl == null)
            {
                // In future this might automatically calculate the srcurl.
                throw new ApplicationException("The srcurl was null, but there was no cached object available.");
            }

            // Ensure the directory exists for the target file.
            string parent = (new DirectoryInfo(path)).Parent.FullName;
            if (!Directory.Exists(parent))
                Directory.CreateDirectory(parent);

            // Download the data from the srcurl and save it at the cache path.
            byte[] data = Downloader.Grab(srcurl);
            StreamWriter writer = new StreamWriter(path, false, Manager.Encoding);
            writer.Write(Manager.Encoding.GetString(data));
            writer.Close();
            return data;
        }

        /// <summary>
        /// Fetchs a raw file from the cache, or downloads the new data from
        /// the specified url and places it in the cache (and returns the path
        /// to the cached object).
        /// </summary>
        /// <param name="url">The URL that the object is located at.</param>
        /// <returns>The path to the file.</returns>
        internal static string GetRawData(string url)
        {
            Cache.EnsureCacheConsistancy();
            Uri uri = new Uri(url);
            string uripath = uri.PathAndQuery;
            if (uripath.IndexOf('?') != -1)
                uripath = uripath.Split('?')[0];
            string local = Cache.GetPathToCachedObject(uri.Scheme + "://" + uri.Host, uripath.TrimStart('/'));

            if (File.Exists(local))
            {
                // Use the cached object.
                return local;
            }
            else
            {
                // Ensure the directory exists for the target file.
                string parent = (new DirectoryInfo(local)).Parent.FullName;
                if (!Directory.Exists(parent))
                    Directory.CreateDirectory(parent);

                // Download the data from the srcurl and save it at the cache path.
                byte[] data = Downloader.Grab(url);
                StreamWriter writer = new StreamWriter(local, false, Manager.Encoding);
                writer.Write(Manager.Encoding.GetString(data));
                writer.Close();
                return local;
            }
        }

        /// <summary>
        /// Updates the cache version of the data with the data stored in the Application
        /// instance.  Does not retrieve new data from the remote repository.
        /// </summary>
        /// <param name="app"></param>
        internal static void UpdateApplicationData(Application app)
        {
            Cache.EnsureCacheConsistancy();
            string path = Cache.GetPathToCachedXML(app.Repository.Url, app.ID);

            StreamWriter writer = new StreamWriter(path, false, Manager.Encoding);
            writer.Write(app.XmlTree.ToXml());
            writer.Close();
        }

        /// <summary>
        /// Fetchs a new copy of the application data from the repository, syncing the
        /// existing Application instance with the new data (and then syncs the cache
        /// with the new information as well).
        /// </summary>
        /// <param name="app"></param>
        internal static void FetchApplicationData(Application app)
        {
            Cache.EnsureCacheConsistancy();
            string path = Cache.GetPathToCachedXML(app.Repository.Url, app.ID);

            if (app.Url == null)
                throw new ApplicationException("The app.Url was null, but a remote update operation was requested.");

            // Download the new data from the app.Url and read the new information out
            // of it.
            byte[] data = null;
            try
            {
                data = Downloader.Grab(app.Url);
            }
            catch (WebException ex)
            {
                // Invalidate the application because we know it's not
                // up-to-date, but the new data is invalid.
                app.Available = false;
                return;
            }

            // Now read the new information out.
            ConfigurationReader config = new ConfigurationReader();
            Application newapp = config.GetApplicationData(data, app.Repository, app.Url);

            // Check to see whether the new information is valid.
            if (newapp == null)
            {
                // Invalidate the application because we know it's not
                // up-to-date, but the new data is invalid.
                app.Available = false;
                return;
            }

            // Clear the image data out of the cache too..
            if (newapp.Image != null)
            {
                string imagepath = Cache.GetPathToCachedObject(app.Repository.Url, newapp.ImageSource);
                if (File.Exists(imagepath))
                    File.Delete(imagepath);

                // Now fetch the new version.
                string localpath = Cache.GetRawData(newapp.ImageSource);
                newapp.ReloadImage(localpath);
            }

            // Now sync the application with the new data.
            app.SyncNew(newapp);
        }

        /// <summary>
        /// Returns the path to a cached XML document based on the repository and ID of the application.
        /// </summary>
        /// <param name="repo">The repository URL.</param>
        /// <param name="id">The ID of the application.</param>
        /// <returns>The local path to the cached XML document (or where it should be saved).</returns>
        private static string GetPathToCachedXML(string repo, string id)
        {
            if (repo.StartsWith("http://"))
                return "Cache/" + repo.Replace("http://", "").Replace("/", "_").Replace("\\", "_")
                    .Replace(":", "_").Replace("*", "_").Replace("?", "_").Replace("\"", "_")
                    .Replace("<", "_").Replace(">", "_").Replace("|", "_") + ".insecure/" + id.ToString() + ".xml";
            else if (repo.StartsWith("https://"))
                return "Cache/" + repo.Replace("https://", "").Replace("/", "_").Replace("\\", "_")
                    .Replace(":", "_").Replace("*", "_").Replace("?", "_").Replace("\"", "_")
                    .Replace("<", "_").Replace(">", "_").Replace("|", "_") + ".secure/" + id.ToString() + ".xml";
            else
            {
                try
                {
                    return "Cache/" + repo.Substring(repo.IndexOf("/") + 1).Replace("/", "_").Replace("\\", "_")
                        .Replace(":", "_").Replace("*", "_").Replace("?", "_").Replace("\"", "_")
                        .Replace("<", "_").Replace(">", "_").Replace("|", "_") + ".unknown/" + id.ToString() + ".xml";
                }
                catch (ArgumentOutOfRangeException)
                {
                    return "Cache/" + repo.Replace("/", "_").Replace("\\", "_")
                        .Replace(":", "_").Replace("*", "_").Replace("?", "_").Replace("\"", "_")
                        .Replace("<", "_").Replace(">", "_").Replace("|", "_") + ".unknown/" + id.ToString() + ".xml";
                }
            }
        }

        /// <summary>
        /// Returns the path to a cached object based on the repository and ID of the application.
        /// </summary>
        /// <param name="repo">The repository URL.</param>
        /// <param name="id">The ID of the application.</param>
        /// <returns>The local path to the cached object (or where it should be saved).</returns>
        private static string GetPathToCachedObject(string repo, string path)
        {
            if (repo.StartsWith("http://"))
                return "Cache/" + repo.Replace("http://", "").Replace("/", "_").Replace("\\", "_")
                    .Replace(":", "_").Replace("*", "_").Replace("?", "_").Replace("\"", "_")
                    .Replace("<", "_").Replace(">", "_").Replace("|", "_") + ".insecure/" + path.Replace('/', '.');
            else if (repo.StartsWith("https://"))
                return "Cache/" + repo.Replace("https://", "").Replace("/", "_").Replace("\\", "_")
                    .Replace(":", "_").Replace("*", "_").Replace("?", "_").Replace("\"", "_")
                    .Replace("<", "_").Replace(">", "_").Replace("|", "_") + ".secure/" + path.Replace('/', '.');
            else
            {
                try
                {
                    return "Cache/" + repo.Substring(repo.IndexOf("/") + 1).Replace("/", "_").Replace("\\", "_")
                        .Replace(":", "_").Replace("*", "_").Replace("?", "_").Replace("\"", "_")
                        .Replace("<", "_").Replace(">", "_").Replace("|", "_") + ".unknown/" + path.Replace('/', '.');
                }
                catch (ArgumentOutOfRangeException)
                {
                    return "Cache/" + repo.Replace("/", "_").Replace("\\", "_")
                        .Replace(":", "_").Replace("*", "_").Replace("?", "_").Replace("\"", "_")
                        .Replace("<", "_").Replace(">", "_").Replace("|", "_") + ".unknown/" + path.Replace('/', '.');
                }
            }
        }
    }
}
